﻿'Copyright (c) 2010 Grailtek

'Permission is hereby granted, free of charge, to any person
'obtaining a copy of this software and associated documentation
'files (the "Software"), to deal in the Software without
'restriction, including without limitation the rights to use,
'copy, modify, merge, publish, distribute, sublicense, and/or sell
'copies of the Software, and to permit persons to whom the
'Software is furnished to do so, subject to the following
'conditions:

'The above copyright notice and this permission notice shall be
'included in all copies or substantial portions of the Software.

'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
'EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
'OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
'NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
'HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
'WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
'FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
'OTHER DEALINGS IN THE SOFTWARE.

Imports System.IO
Imports System.Security.Policy
Imports System.Reflection
Imports System.Aspect

Public Module AppDomainUtils

    Private ReadOnly AssemblyLoaderType As Type = GetType(AssemblyLoader)

    ''' <summary>
    ''' Executes a method within the context of a temporary domain where the specified assembly is loaded.
    ''' If the method is not static, its declaring type must be serializable.
    ''' </summary>
    ''' <param name="assemblyFileName"></param>
    ''' <param name="Action"></param>
    ''' <remarks></remarks>
    Public Sub ProcessAssembly(ByVal assemblyFileName As String, ByVal Action As Action(Of Assembly))
        Dim assemblyFileInfo As FileInfo = New FileInfo(assemblyFileName)
        If Not assemblyFileInfo.Exists Then Throw New FileNotFoundException(assemblyFileName)
        Dim dom As AppDomain = CreateDomain()
        Try
            Dim loader = DirectCast(dom.CreateInstanceFrom(AssemblyLoaderType.Assembly.Location, AssemblyLoaderType.FullName).Unwrap(), AssemblyLoader)
            loader.LoadAssembly(assemblyFileInfo.FullName)
            loader.ProcessAssembly(assemblyFileInfo.FullName, Action)
        Finally
            AppDomain.Unload(dom)
        End Try
    End Sub

    ''' <summary>
    ''' Executes a function within the context of a temporary domain where the specified assembly is loaded.
    ''' The function's return type must be serializable. If the function is not static, its declaring type must be serializable.
    ''' </summary>
    ''' <param name="assemblyFileName"></param>
    ''' <param name="Action"></param>
    ''' <remarks></remarks>
    Public Function ProcessAssembly(Of T)(ByVal assemblyFileName As String, ByVal Action As Func(Of Assembly, T)) As T
        Dim assemblyFileInfo As FileInfo = New FileInfo(assemblyFileName)
        If Not assemblyFileInfo.Exists Then Throw New FileNotFoundException(assemblyFileName)
        Dim dom As AppDomain = CreateDomain()
        Try
            Dim loader = DirectCast(dom.CreateInstanceFrom(AssemblyLoaderType.Assembly.Location, AssemblyLoaderType.FullName).Unwrap(), AssemblyLoader)
            loader.LoadAssembly(assemblyFileInfo.FullName)
            Return loader.ProcessAssembly(assemblyFileInfo.FullName, Action)
        Finally
            AppDomain.Unload(dom)
        End Try
    End Function

    Private Function CreateDomain() As AppDomain
        Dim CurrentDomain As AppDomain = AppDomain.CurrentDomain
        Return AppDomain.CreateDomain("dyn__AppDomainUtilsDomain", New Evidence(CurrentDomain.Evidence), CurrentDomain.SetupInformation)
    End Function

    Private Class AssemblyLoader
        Inherits MarshalByRefObject

        Public Sub ProcessAssembly(ByVal AssyFileName As String, ByVal Action As Action(Of Assembly))
            Dim path = System.IO.Path.GetDirectoryName(AssyFileName)
            Dim directory = New DirectoryInfo(path)
            Dim resolveEventHandler As New ResolveEventHandler(Function(sender As Object, e As ResolveEventArgs) OnAssemblyResolve(e, directory))
            AddHandler AppDomain.CurrentDomain.AssemblyResolve, resolveEventHandler
            Dim assy As Assembly = AppDomain.CurrentDomain.GetAssemblies().Where(Function(a) a.Location = AssyFileName).First
            Try
                Action.Invoke(assy)
            Finally
                RemoveHandler AppDomain.CurrentDomain.AssemblyResolve, resolveEventHandler
            End Try
        End Sub

        Public Function ProcessAssembly(Of T)(ByVal AssyFileName As String, ByVal Action As Func(Of Assembly, T)) As T
            Dim path = System.IO.Path.GetDirectoryName(AssyFileName)
            Dim directory = New DirectoryInfo(path)
            Dim resolveEventHandler As New ResolveEventHandler(Function(sender As Object, e As ResolveEventArgs) OnAssemblyResolve(e, directory))
            AddHandler AppDomain.CurrentDomain.AssemblyResolve, resolveEventHandler
            Dim assy As Assembly = AppDomain.CurrentDomain.GetAssemblies().Where(Function(a) a.Location = AssyFileName).First
            Try
                Return Action.Invoke(assy)
            Finally
                RemoveHandler AppDomain.CurrentDomain.AssemblyResolve, resolveEventHandler
            End Try
        End Function

        Private Function OnAssemblyResolve(ByVal args As ResolveEventArgs, ByVal directory As DirectoryInfo) As Assembly
            Dim ret As Assembly
            Dim loadedAssembly As Assembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(Function(asm) String.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase))
            If Not loadedAssembly Is Nothing Then
                ret = loadedAssembly
            Else
                Dim assemblyName As New AssemblyName(args.Name)
                Dim dependentAssemblyFilename As String = Path.Combine(directory.FullName, assemblyName.Name & ".dll")
                If File.Exists(dependentAssemblyFilename) Then
                    ret = Assembly.LoadFrom(dependentAssemblyFilename)
                Else
                    ret = Assembly.Load(args.Name)
                End If
            End If
            Return ret
        End Function

        Friend Sub LoadAssembly(ByVal assemblyPath As [String])
            Try
                Assembly.LoadFrom(assemblyPath)
            Catch ex As FileNotFoundException
                'ignore
            End Try
        End Sub

    End Class

End Module
